using System;
using System.Collections.Generic;
using CriticalSystems.Instrumentation.Internal;
using PostSharp.CodeModel;
using PostSharp.CodeModel.Helpers;
using PostSharp.CodeWeaver;
using PostSharp.Collections;
using PostSharp.Extensibility;
using PostSharp.Extensibility.Tasks;

namespace CriticalSystems.Instrumentation.Weaver
{
    /// <summary>
    /// Implementation of TracingAttribute. Weaves tracing code into procedures.
    /// </summary>
    /// <remarks>
    /// <para>
    ///     This task implements the <see cref="IAdviceProvider"/> interface. The <see cref="WeaverTask"/>
    ///     class calls our <see cref="ProvideAdvices"/> method, which emits an advice for each traced method. 
    ///     Then, the <see cref="PostSharp.CodeWeaver.Weaver"/> class calls our advice class.
    /// </para>
    /// <para>
    ///     Each instance of the <see cref="TracingAttribute"/> custom attribute is associated to an advice. The join points
    ///     are the entry and the exit of the body of all methods having the <see cref="TracingAttribute"/>.
    /// </para>
    /// </remarks>
    public class InstrumentationTask : Task, IAdviceProvider
    {
        private IMethod logConstructor;
        private IMethod logErrorExceptionMethod;
        private IMethod logErrorMethod;
        private IMethod loggingErrorExceptionMethod;
        private IMethod loggingErrorMethod;
        private IMethod loggingInfoMethod;
        private IMethod loggingWarningMethod;
        private IMethod logInfoMethod;
        private IMethod logInMethod;
        private IMethod logOutExceptionMethod;
        private IMethod logOutMethod;
        private ITypeSignature logType;
        private IMethod logWarningMethod;
        private IMethod traceConstructor;
        private IMethod traceErrorExceptionMethod;
        private IMethod traceErrorMethod;
        private IMethod traceInfoMethod;
        private IMethod traceInMethod;
        private IMethod traceInMethodVariadic;
        private IMethod traceOutExceptionMethod;
        private IMethod traceOutMethod;
        private IMethod traceOutMethodVariadic;
        private ITypeSignature traceType;
        private IMethod traceWarningMethod;
        private IMethod tracingErrorExceptionMethod;
        private IMethod tracingErrorMethod;
        private IMethod tracingFlagMethod;
        private IMethod tracingInfoMethod;
        private IMethod tracingWarningMethod;

        public IMethod TraceInMethod
        {
            get { return this.traceInMethod; }
        }

        public IMethod TraceOutMethod
        {
            get { return this.traceOutMethod; }
        }

        public IMethod TraceOutExceptionMethod
        {
            get { return this.traceOutExceptionMethod; }
        }

        public IMethod TraceInMethodVariadic
        {
            get { return this.traceInMethodVariadic; }
        }

        public IMethod TracingFlagMethod
        {
            get { return this.tracingFlagMethod; }
        }
        public IMethod TraceOutMethodVariadic
        {
            get { return this.traceOutMethodVariadic; }
        }
        public ITypeSignature TraceType
        {
            get { return this.traceType; }
        }

        public IMethod TraceConstructor
        {
            get { return this.traceConstructor; }
        }
        public IMethod LogInMethod
        {
            get { return this.logInMethod; }
        }
        public IMethod LogOutMethod
        {
            get { return this.logOutMethod; }
        }
        public IMethod LogOutExceptionMethod
        {
            get { return this.logOutExceptionMethod; }
        }
        public IMethod LogConstructor
        {
            get { return this.logConstructor; }
        }
        public ITypeSignature LogType
        {
            get { return this.logType; }
        }
        public IMethod TraceInfoMethod
        {
            get { return this.traceInfoMethod; }
        }
        public IMethod TraceErrorMethod
        {
            get { return this.traceErrorMethod; }
        }
        public IMethod TraceErrorExceptionMethod
        {
            get { return this.traceErrorExceptionMethod; }
        }
        public IMethod TraceWarningMethod
        {
            get { return this.traceWarningMethod; }
        }
        public IMethod LogInfoMethod
        {
            get { return this.logInfoMethod; }
        }
        public IMethod LogErrorMethod
        {
            get { return this.logErrorMethod; }
        }
        public IMethod LogErrorExceptionMethod
        {
            get { return this.logErrorExceptionMethod; }
        }
        public IMethod LogWarningMethod
        {
            get { return this.logWarningMethod; }
        }
        public IMethod LoggingInfoMethod
        {
            get { return this.loggingInfoMethod; }
        }
        public IMethod LoggingWarningMethod
        {
            get { return this.loggingWarningMethod; }
        }
        public IMethod LoggingErrorMethod
        {
            get { return this.loggingErrorMethod; }
        }
        public IMethod LoggingErrorExceptionMethod
        {
            get { return this.loggingErrorExceptionMethod; }
        }
        public IMethod TracingInfoMethod
        {
            get { return this.tracingInfoMethod; }
        }
        public IMethod TracingWarningMethod
        {
            get { return this.tracingWarningMethod; }
        }
        public IMethod TracingErrorMethod
        {
            get { return this.tracingErrorMethod; }
        }
        public IMethod TracingErrorExceptionMethod
        {
            get { return this.tracingErrorExceptionMethod; }
        }

        #region IAdviceProvider Members
        public void ProvideAdvices(PostSharp.CodeWeaver.Weaver codeWeaver)
        {
            AddAttributeAdvice(codeWeaver);
            AddExplicitAdvice(codeWeaver);
        }
        #endregion

        private void AddExplicitAdvice(PostSharp.CodeWeaver.Weaver weaver)
        {
            IEnumerator<MetadataDeclaration> metadata = Project.Module.GetDeclarationEnumerator(TokenType.MethodDef);
            IList<MethodDefDeclaration> methods = new List<MethodDefDeclaration>();
            while (metadata.MoveNext())
            {
                MethodDefDeclaration method = metadata.Current as MethodDefDeclaration;
                if (method != null)
                {
                    methods.Add(method);
                }
            }
            IMethod tracingMethod = this.tracingInfoMethod;
            InstrumentationAdvice advice = new InstrumentationAdvice(this, tracingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(tracingMethod.GetMethodDefinition()));
            tracingMethod = this.tracingWarningMethod;
            advice = new InstrumentationAdvice(this, tracingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(tracingMethod.GetMethodDefinition()));
            tracingMethod = this.tracingErrorMethod;
            advice = new InstrumentationAdvice(this, tracingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(tracingMethod.GetMethodDefinition()));
            tracingMethod = this.tracingErrorExceptionMethod;
            advice = new InstrumentationAdvice(this, tracingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(tracingMethod.GetMethodDefinition()));

            IMethod loggingMethod = this.loggingInfoMethod;
            advice = new InstrumentationAdvice(this, loggingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(loggingMethod.GetMethodDefinition()));
            loggingMethod = this.loggingWarningMethod;
            advice = new InstrumentationAdvice(this, loggingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(loggingMethod.GetMethodDefinition()));
            loggingMethod = this.loggingErrorMethod;
            advice = new InstrumentationAdvice(this, loggingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(loggingMethod.GetMethodDefinition()));
            loggingMethod = this.loggingErrorExceptionMethod;
            advice = new InstrumentationAdvice(this, loggingMethod);
            weaver.AddMethodLevelAdvice(advice, methods, JoinPointKinds.InsteadOfCall,
                                        new Singleton<MetadataDeclaration>(loggingMethod.GetMethodDefinition()));
        }

        private void AddAttributeAdvice(PostSharp.CodeWeaver.Weaver codeWeaver)
        {
            // Gets the dictionary of custom attributes.
            CustomAttributeDictionaryTask customAttributeDictionary = CustomAttributeDictionaryTask.GetTask(Project);

            // Requests an enumerator of all instances of our TracingAttribute.
            IEnumerator<ICustomAttributeInstance> tracingAttributeEnumerator =
                customAttributeDictionary.GetCustomAttributesEnumerator(typeof (TracingAttribute), false);

            // Requests an enumerator of all instances of our LoggingAttribute.
            IEnumerator<ICustomAttributeInstance> loggingAttributeEnumerator =
                customAttributeDictionary.GetCustomAttributesEnumerator(typeof (LoggingAttribute), false);

            IDictionary<MethodDefDeclaration, InstrumentationAdvice> methodAdvice =
                new Dictionary<MethodDefDeclaration, InstrumentationAdvice>();

            // For each instance of our TracingAttribute.
            while (loggingAttributeEnumerator.MoveNext())
            {
                // Gets the method to which it applies.
                MethodDefDeclaration methodDef = loggingAttributeEnumerator.Current.TargetElement
                                                 as MethodDefDeclaration;

                if (methodDef != null)
                {
                    LoggingAttribute loggingAttribute =
                        (LoggingAttribute)
                        CustomAttributeHelper.ConstructRuntimeObject(loggingAttributeEnumerator.Current.Value,
                                                                     Project.Module);
                    Utilities.DebugWriteLine("Logging attribute on {0}.{1}. Logging type {2}",
                                             methodDef.DeclaringType.Name, methodDef.Name, loggingAttribute.LoggingType);

                    InstrumentationAdvice advice;
                    if (!methodAdvice.TryGetValue(methodDef, out advice))
                    {
                        advice = new InstrumentationAdvice(this);
                        methodAdvice[methodDef] = advice;
                    }
                    if (advice.LoggingAttribute == null)
                    {
                        advice.LoggingAttribute = loggingAttribute;
                    }
                    else
                    {
                        advice.LoggingAttribute.LoggingType |= loggingAttribute.LoggingType;
                        if (advice.LoggingAttribute.Name == null && loggingAttribute.Name != null)
                        {
                            advice.LoggingAttribute.Name = loggingAttribute.Name;
                        }
                    }
                }
            }

            // For each instance of our LoggingAttribute.
            while (tracingAttributeEnumerator.MoveNext())
            {
                // Gets the method to which it applies.
                MethodDefDeclaration methodDef = tracingAttributeEnumerator.Current.TargetElement
                                                 as MethodDefDeclaration;

                if (methodDef != null)
                {
                    InstrumentationAdvice advice;
                    if (!methodAdvice.TryGetValue(methodDef, out advice))
                    {
                        advice = new InstrumentationAdvice(this);
                        methodAdvice[methodDef] = advice;
                    }
                    // Constructs a custom attribute instance.
                    TracingAttribute tracingAttribute =
                        (TracingAttribute)
                        CustomAttributeHelper.ConstructRuntimeObject(tracingAttributeEnumerator.Current.Value,
                                                                     Project.Module);
                    advice.TracingAttribute = tracingAttribute;
                }
            }

            foreach (KeyValuePair<MethodDefDeclaration, InstrumentationAdvice> pair in methodAdvice)
            {
                codeWeaver.AddMethodLevelAdvice(pair.Value,
                                                new Singleton<MethodDefDeclaration>(pair.Key),
                                                JoinPointKinds.BeforeMethodBody |
                                                JoinPointKinds.AfterMethodBodySuccess |
                                                JoinPointKinds.AfterMethodBodyException,
                                                null);
            }
        }

        /// <summary>
        /// Initializes the task.
        /// </summary>
        /// <remarks>
        /// This method is called once per module. It caches what it can. Concretely, this method looks 
        /// for the tracing methods.
        /// </remarks>
        protected override void Initialize()
        {
            ModuleDeclaration module = Project.Module;
            FindTracingMethods(module);
            FindLoggingMethods(module);
        }

        private void FindLoggingMethods(ModuleDeclaration module)
        {
            this.logInMethod =
                module.FindMethod(typeof (Log).GetMethod("In", new Type[] {typeof (string)}), BindingOptions.Default);
            this.logOutMethod =
                module.FindMethod(typeof (Log).GetMethod("Out", new Type[] {typeof (string)}),
                                  BindingOptions.Default);
            this.logOutExceptionMethod =
                module.FindMethod(typeof (Log).GetMethod("Out", new Type[] {typeof (string), typeof (Exception)}),
                                  BindingOptions.Default);
            this.logType = module.FindType(typeof (Log), BindingOptions.Default);

            this.logConstructor =
                module.FindMethod(typeof(Log).GetConstructor(new Type[] { typeof(string), typeof(string) }),
                                  BindingOptions.Default);
            this.loggingInfoMethod =
                module.FindMethod(typeof (Logging).GetMethod("Info", new Type[] {typeof (string), typeof (object[])}),
                                  BindingOptions.Default);
            this.loggingWarningMethod =
                module.FindMethod(
                    typeof (Logging).GetMethod("Warning", new Type[] {typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.loggingErrorMethod =
                module.FindMethod(typeof (Logging).GetMethod("Error", new Type[] {typeof (string), typeof (object[])}),
                                  BindingOptions.Default);
            this.loggingErrorExceptionMethod =
                module.FindMethod(typeof (Logging).GetMethod("Error", new Type[] {typeof (Exception)}),
                                  BindingOptions.Default);
            this.logInfoMethod =
                module.FindMethod(
                    typeof (Log).GetMethod("Info", new Type[] {typeof (string), typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.logWarningMethod =
                module.FindMethod(
                    typeof (Log).GetMethod("Warning", new Type[] {typeof (string), typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.logErrorMethod =
                module.FindMethod(
                    typeof (Log).GetMethod("Error", new Type[] {typeof (string), typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.logErrorExceptionMethod =
                module.FindMethod(typeof (Log).GetMethod("Error", new Type[] {typeof (string), typeof (Exception)}),
                                  BindingOptions.Default);
        }

        private void FindTracingMethods(ModuleDeclaration module)
        {
            this.traceInMethod =
                module.FindMethod(typeof (Trace).GetMethod("In", new Type[] {typeof (string)}), BindingOptions.Default);
            this.traceInMethodVariadic =
                module.FindMethod(typeof (Trace).GetMethod("In", new Type[] {typeof (string), typeof (object[])}),
                                  BindingOptions.Default);
            this.traceOutMethod =
                module.FindMethod(typeof (Trace).GetMethod("Out", new Type[] {typeof (string)}),
                                  BindingOptions.Default);
            this.traceOutMethodVariadic =
                module.FindMethod(typeof (Trace).GetMethod("Out", new Type[] {typeof (string), typeof (object[])}),
                                  BindingOptions.Default);
            this.traceOutExceptionMethod =
                module.FindMethod(typeof (Trace).GetMethod("Out", new Type[] {typeof (string), typeof (Exception)}),
                                  BindingOptions.Default);
            this.tracingFlagMethod = module.FindMethod(typeof (Trace).GetMethod("get_Flags", new Type[] {}),
                                                       BindingOptions.Default);
            this.traceType = module.FindType(typeof (Trace), BindingOptions.Default);

            this.traceConstructor =
                module.FindMethod(typeof (Trace).GetConstructor(new Type[] {typeof (string)}),
                                  BindingOptions.Default);
            this.tracingInfoMethod =
                module.FindMethod(typeof (Tracing).GetMethod("Info", new Type[] {typeof (string), typeof (object[])}),
                                  BindingOptions.Default);
            this.tracingWarningMethod =
                module.FindMethod(
                    typeof (Tracing).GetMethod("Warning", new Type[] {typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.tracingErrorMethod =
                module.FindMethod(typeof (Tracing).GetMethod("Error", new Type[] {typeof (string), typeof (object[])}),
                                  BindingOptions.Default);
            this.tracingErrorExceptionMethod =
                module.FindMethod(typeof (Tracing).GetMethod("Error", new Type[] {typeof (Exception)}),
                                  BindingOptions.Default);
            this.traceInfoMethod =
                module.FindMethod(
                    typeof (Trace).GetMethod("Info", new Type[] {typeof (string), typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.traceWarningMethod =
                module.FindMethod(
                    typeof (Trace).GetMethod("Warning", new Type[] {typeof (string), typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.traceErrorMethod =
                module.FindMethod(
                    typeof (Trace).GetMethod("Error", new Type[] {typeof (string), typeof (string), typeof (object[])}),
                    BindingOptions.Default);
            this.traceErrorExceptionMethod =
                module.FindMethod(typeof (Trace).GetMethod("Error", new Type[] {typeof (string), typeof (Exception)}),
                                  BindingOptions.Default);
        }
    }
}